﻿//
// © Algobox Ltd - Benn Eichhorn
//
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Concurrent;
using Algobox.Objects.Finance.Prices;

namespace Algobox.Objects.Finance.Depth
{
    public class Depth
    {
        #region Constructors

        public Depth()
        {
            _bidPrices = new ConcurrentDictionary<double, ulong>();
            _askPrices = new ConcurrentDictionary<double, ulong>();
            _orders = new ConcurrentDictionary<string, DepthItem>();
            _bid = new PriceVolume(PriceVolume.PriceType.Bid);
            _ask = new PriceVolume(PriceVolume.PriceType.Ask);
            _last = new PriceVolume(PriceVolume.PriceType.Last);
        }


        public Depth(PriceVolume bid, PriceVolume ask, PriceVolume last)
        {
            _bidPrices = new ConcurrentDictionary<double, ulong>();
            _askPrices = new ConcurrentDictionary<double, ulong>();
            _orders = new ConcurrentDictionary<string, DepthItem>();
            _bid = bid;
            _ask = ask;
            _last = last;
        }

        #endregion
        //////////////////
        #region Member Variables

        double _bestBidPrice;
        ulong _bestBidVolume;
        double _bestAskPrice;
        ulong _bestAskVolume;
        ConcurrentDictionary<double, ulong> _bidPrices;
        ConcurrentDictionary<double, ulong> _askPrices;
        ConcurrentDictionary<string, DepthItem> _orders;
        PriceVolume _bid;
        PriceVolume _ask;
        PriceVolume _last;

        private class DepthItem
        {
            public DepthItem(string orderId, bool isBuy, double price, ulong quantity, DateTime timeStamp)
            {
                OrderId = orderId;
                IsBuy = isBuy;
                Price = price;
                Quantity = quantity;
                TimeStamp = timeStamp;
            }
            public string OrderId;
            public bool IsBuy;
            public double Price;
            public ulong Quantity;
            public DateTime TimeStamp;
        }

        #endregion
        //////////////////
        #region Properties



        #endregion
        //////////////////
        #region Functions

        string _lastMessage;

        /// <summary>
        /// Adds an order to the depth of this book
        /// </summary>
        /// <param name="orderId">The reference to the order</param>
        /// <param name="isBuy">Bid or Ask order, true for bid</param>
        /// <param name="price">The current price of the order</param>
        /// <param name="quantity">The number of shares of the order</param>
        /// <param name="timeStamp">Time this order was added</param>
        /// <param name="best">If a new best is set it will be avaliable as this out paramater or null otherwise</param>
        /// <returns>True when a new best price is set</returns>
        public bool OrderAdd(string orderId, bool isBuy, double price, ulong quantity, DateTime timeStamp, out PriceVolume best)
        {

            DepthItem item = _orders.AddOrUpdate(orderId,
                (key) =>
                {
                    return new DepthItem(orderId, isBuy, price, quantity, timeStamp);
                },
                (key, value) =>
                {
                    ulong volume;
                    // update this price level
                    if (isBuy)
                    {
                        volume = _bidPrices.AddOrUpdate(value.Price, 0, (p, v) => { return v - value.Quantity; });
                        if (volume == 0 && value.Price != price)
                        {
                            _bidPrices.TryRemove(value.Price, out volume);

                            // do we need to calculate a new best bid?
                            if (_bestBidPrice == value.Price)
                            {
                                if (_bidPrices.IsEmpty)
                                {
                                    _bestBidPrice = 0;
                                    _bestBidVolume = 0;
                                    _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                                }
                                else
                                {
                                    _bestBidPrice = _bidPrices.Keys.Max();
                                    _bestBidVolume = _bidPrices[_bestBidPrice];
                                    _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                                }
                            }
                        }                       
                    }
                    else
                    {
                        volume = _askPrices.AddOrUpdate(value.Price, 0, (p, v) => { return v - value.Quantity; });
                        if (volume == 0 && value.Price != price)
                        {
                            _askPrices.TryRemove(value.Price, out volume);

                            // do we need to calculate a new best ask?
                            if (_bestAskPrice == value.Price)
                            {
                                if (_askPrices.IsEmpty)
                                {
                                    _bestAskPrice = 0;
                                    _bestAskVolume = 0;
                                    _ask.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                                }
                                else
                                {
                                    _bestAskPrice = _askPrices.Keys.Min();
                                    _bestAskVolume = _askPrices[_bestAskPrice];
                                    _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);
                                }
                            }
                        }  
                    }
                    
                    // update the values of the item                    
                    return value;
                });
            
            _lastMessage = "OrderAdd " + orderId;

            item.Price = price;
            item.Quantity = quantity;
            item.TimeStamp = timeStamp;

            if (isBuy)
            {
                // update the bid price level
                _bidPrices.AddOrUpdate(price, quantity, (key, value) => { return value + quantity; });
                
                // are we the best?
                if (_bestBidPrice == price)
                {
                    _bestBidVolume += quantity;

                    // update the best bid
                    _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);

                    best = _bid;

                    return true;
                }
                else if (item.Price > _bestBidPrice)
                {
                    _bestBidPrice = item.Price;
                    _bestBidVolume = quantity;

                    // update the best bid
                    _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);

                    best = _bid;

                    return true;
                }
            }
            else
            {
                // update the ask price level
                _askPrices.AddOrUpdate(price, quantity, (key, value) => { return value + quantity; });

                // are we the best?
                if (_bestAskPrice == price)
                {
                    _bestAskVolume += quantity;
                    
                    // update the best ask
                    _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);

                    best = _ask;

                    return true;
                }
                else if (item.Price < _bestAskPrice || _bestAskPrice == 0)
                {
                    _bestAskPrice = item.Price;
                    _bestAskVolume = quantity;

                    // update the best ask
                    _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);

                    best = _ask;

                    return true;
                }
            }

            best = null;

            return false;
        }

        /// <summary>
        /// Add an order execution to the depth
        /// </summary>
        /// <param name="orderId">The reference to the order</param>
        /// <param name="executedQuantity">Number of share executed, to be removed from the depth</param>
        /// <param name="timeStamp">Time stamp of the execution</param>
        /// <param name="last">If the order is found then this out parameter will set to the last execution otherwise set to null</param>
        /// <param name="best">If a new best is set it will be avaliable as this out paramater or null otherwise</param>
        /// <returns>Returns true if a new best price is set</returns>
        public bool OrderExecution(string orderId, ulong executedQuantity, DateTime timeStamp, out PriceVolume last, out PriceVolume best, out bool isComplete)
        {
            _lastMessage = "OrderExecution " + orderId;

            DepthItem item;
            if (_orders.TryGetValue(orderId, out item))
            {
                _last.Update(item.Price, executedQuantity, timeStamp);
                last = _last;

                ulong quantity;
                if (item.IsBuy)
                {
                    // update this price level
                    quantity = _bidPrices.AddOrUpdate(item.Price, executedQuantity, (key, value) => { return value - executedQuantity; });

                    if (quantity == 0)
                    {
                        isComplete = true;

                        // remove the depth item
                        _orders.TryRemove(orderId, out item);

                        // remove this price level
                        _bidPrices.TryRemove(item.Price, out quantity);

                        // do we need to calculate a new best bid?
                        if (_bestBidPrice == item.Price)
                        {
                            if (_bidPrices.IsEmpty)
                            {
                                _bestBidPrice = 0;
                                _bestBidVolume = 0;
                                _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                                best = null;
                                return false;
                            }

                            _bestBidPrice = _bidPrices.Keys.Max();
                            _bestBidVolume = _bidPrices[_bestBidPrice];
                            _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                            best = _bid;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                    else
                    {
                        // remove the quantity from this order
                        item.Quantity -= executedQuantity;

                        if (item.Quantity == 0)
                        {
                            isComplete = true;
                            _orders.TryRemove(orderId, out item);
                        }
                        else
                        {
                            isComplete = false;
                            item.TimeStamp = timeStamp;
                        }

                        // have we updated the best bid?
                        if (_bestBidPrice == item.Price)
                        {
                            _bestBidVolume -= executedQuantity;
                            _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                            best = _bid;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                }
                else
                {
                    // update this price level
                    quantity = _askPrices.AddOrUpdate(item.Price, executedQuantity, (key, value) => { return value - executedQuantity; });

                    if (quantity == 0)
                    {
                        isComplete = true;

                        // remove the depth item
                        _orders.TryRemove(orderId, out item);

                        // remove this price level
                        _askPrices.TryRemove(item.Price, out quantity);

                        // do we need to calculate a new best bid?
                        if (_bestAskPrice == item.Price)
                        {
                            if (_askPrices.IsEmpty)
                            {
                                _bestAskPrice = 0;
                                _bestAskVolume = 0;
                                _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);
                                best = null;
                                return false;
                            }

                            _bestAskPrice = _askPrices.Keys.Min();
                            _bestAskVolume = _askPrices[_bestAskPrice];
                            _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);
                            best = _ask;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                    else
                    {
                        isComplete = false;

                        // remove the quantity from this order
                        item.Quantity -= executedQuantity;

                        if (item.Quantity == 0)
                        {
                            isComplete = true;
                            _orders.TryRemove(orderId, out item);
                        }
                        else
                        {
                            isComplete = false;
                            item.TimeStamp = timeStamp;
                        }

                        // have we updated the best bid?
                        if (_bestAskPrice == item.Price)
                        {
                            _bestAskVolume -= executedQuantity;
                            _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);
                            best = _ask;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                }
            }
            else
            {
                // should never come here in reality
                isComplete = true;
                last = null;
                best = null;
                return false;
            }
        }

        /// <summary>
        /// Cancel whole or part of an order
        /// </summary>
        /// <param name="orderId">The reference to the order</param>
        /// <param name="canceledQuantity">Number of shares canceled, to be removed from the depth</param>
        /// <param name="timeStamp">Time stamp of the execution</param>
        /// <param name="best">If a new best is set it will be avaliable as this out paramater or null otherwise</param>
        /// <returns>Returns true if a new best price is set</returns>
        public bool OrderCancel(string orderId, ulong canceledQuantity, DateTime timeStamp, out PriceVolume best, out bool isComplete)
        {
            _lastMessage = "OrderCancel " + orderId;

            DepthItem item;
            if (_orders.TryGetValue(orderId, out item))
            {
                ulong quantity;
                if (item.IsBuy)
                {
                    // update this price level
                    quantity = _bidPrices.AddOrUpdate(item.Price, canceledQuantity, (key, value) => { return value - canceledQuantity; });

                    if (quantity == 0)
                    {
                        // remove this price level
                        _bidPrices.TryRemove(item.Price, out quantity);

                        // remove the depth item as this must be 0 also
                        _orders.TryRemove(orderId, out item);

                        isComplete = true;

                        // do we need to calculate a new best bid?
                        if (_bestBidPrice == item.Price)
                        {
                            if (_bidPrices.IsEmpty)
                            {
                                _bestBidPrice = 0;
                                _bestBidVolume = 0;
                                _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                                best = null;
                                return false;
                            }

                            _bestBidPrice = _bidPrices.Keys.Max();
                            _bestBidVolume = _bidPrices[_bestBidPrice];
                            _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                            best = _bid;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                    else
                    {
                        // remove the quantity from this order
                        item.Quantity -= canceledQuantity;

                        if (item.Quantity == 0)
                        {
                            isComplete = true;
                            _orders.TryRemove(orderId, out item);
                        }
                        else
                        {
                            isComplete = false;
                            item.TimeStamp = timeStamp;
                        }

                        // have we updated the best bid?
                        if (_bestBidPrice == item.Price)
                        {
                            _bestBidVolume -= canceledQuantity;
                            _bid.Update(_bestBidPrice, _bestBidVolume, timeStamp);
                            best = _bid;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                }
                else
                {
                    // update this price level
                    quantity = _askPrices.AddOrUpdate(item.Price, canceledQuantity, (key, value) => { return value - canceledQuantity; });

                    // if the price level reaches 0 then we should remove it to improve searches for min price
                    if (quantity == 0)
                    {
                        // remove this price level
                        _askPrices.TryRemove(item.Price, out quantity);

                        // remove the depth item as this must also be 0
                        _orders.TryRemove(orderId, out item);

                        isComplete = true;

                        // do we need to calculate a new best ask?
                        if (_bestAskPrice == item.Price)
                        {
                            if (_askPrices.IsEmpty)
                            {
                                _bestAskPrice = 0;
                                _bestAskVolume = 0;
                                _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);
                                best = null;
                                return false;
                            }

                            _bestAskPrice = _askPrices.Keys.Min();
                            _bestAskVolume = _askPrices[_bestAskPrice];
                            _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);
                            best = _ask;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                    else
                    {
                        // remove the quantity from this order
                        item.Quantity -= canceledQuantity;

                        if (item.Quantity == 0)
                        {
                            isComplete = true;
                            _orders.TryRemove(orderId, out item);
                        }
                        else
                        {
                            isComplete = false;
                            item.TimeStamp = timeStamp;
                        }

                        // have we updated the best ask?
                        if (_bestAskPrice == item.Price)
                        {
                            _bestAskVolume -= canceledQuantity;
                            _ask.Update(_bestAskPrice, _bestAskVolume, timeStamp);
                            best = _ask;

                            return true;
                        }
                        best = null;
                        return false;
                    }
                }
            }
            else
            {
                isComplete = true;
                best = null;
                return false;
            }
        }

        #endregion
    }
}
